
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using Swaf;
using Swaf.Config;
using Swaf.Container;
using System.Xml;

namespace Swaf.Crypto
{
    /// <summary>
    /// Summary description for CryptoManager.
    /// </summary>
    public class CryptoManager : ICryptoManager, ICryptoManagerEx, IConfigPartner
    {
        static byte[] s_additionalEntropy = { 9, 8, 7, 6, 5 };

#if !NET_1_1
        protected static readonly DataProtectionScope s_defaultDataProtectionScope = DataProtectionScope.LocalMachine;
        protected static readonly string DataProtectedScopeMacro = "$CryptoDataProtectionScope$";

        protected IX509XmlCrypto m_x509Crypto = null;

#else
		protected static readonly string  FileContainerName = "CSFContainerName";
#endif
		protected string m_keyClass;
        protected BaseKeyMaster m_keys;
        protected string m_defaultKeyName = "";
        protected FlexiMap m_activeCryptoSessions = new FlexiMap();
        protected bool m_useRandomKeys = false;

        protected internal static bool EncryptMacroGenDecrypt = false;

        private const string BEGIN_IDENTIFIER = "KINFO-->";
        private const string END_IDENTIFIER = "<--KINFO";
        private const string USE_RANDOM_KEYS = "$UseRandomKeys$";

        public CryptoManager(IApplication app)
        {
        }

#if !NET_1_1

        public IX509XmlCrypto X509XmlCrypto
        {
            get { return m_x509Crypto; }
            set { m_x509Crypto = value; }
        }

        protected DataProtectionScope DataProtectionScope
        {
            get
            {
                DataProtectionScope scope = s_defaultDataProtectionScope;
                string scopeVal = Application.currentApp.resMgr.resolve(DataProtectedScopeMacro);
                if (string.Compare(scopeVal, DataProtectedScopeMacro, true) != 0)
                {
                    try
                    {
                        scope = (DataProtectionScope)Enum.Parse(typeof(DataProtectionScope), scopeVal, true);
                    }
                    catch (Exception)
                    {
                        // Do nothing since scope preset to the default value
                    }
                }

                return scope;
            }
        }
#endif
        #region ICrypto Members

        public IKeyMaster KeyMaster
        {
            get
            {
                return m_keys;
            }
        }

        public string DefaultKey
        {
            get
            {
                return m_defaultKeyName;
            }
            set
            {
                m_defaultKeyName = value;
            }
        }

        public bool UseRandomKeys
        {
            get { return m_useRandomKeys; }
            set { m_useRandomKeys = value; }
        }

        //internal void setKeyMaster(BaseKeyMaster km)
		internal void setKeyMaster(string km)
        {
			m_keyClass = km;
            //m_keys = km;
        }

        protected ICrypto addActiveSession(string keyName)
        {
            IKeyInfo ki = m_keys.getKeyInfo(keyName);
            ICryptoHandler ch = m_keys.getHandler(ki.Type);
            ICrypto c = ch.startCryptoSession(ki.Key);
            m_activeCryptoSessions[keyName] = c;
            return c;
        }

        public byte[] decrypt(byte[] info)
        {
            return decrypt(m_defaultKeyName, info);
        }

        public byte[] decrypt(string name, byte[] info)
        {
            byte[] encryptedBuffer = info;

            // Initialization
            String keyName = name;

            // Unencode the data to determine if a key is in header
            String unEncodedInfo = Encoding.UTF8.GetString(info);
            Int32 index = unEncodedInfo.IndexOf(END_IDENTIFIER);
            if (index > 0)
            {
                // Get the key name from the encrypted data
                keyName = unEncodedInfo.Substring(0, index);
                keyName = keyName.Substring(BEGIN_IDENTIFIER.Length);

                // Strip the header off of the data before decrypting
                byte[] hdrName = Encoding.UTF8.GetBytes(BEGIN_IDENTIFIER + keyName + END_IDENTIFIER);
                encryptedBuffer = new byte[info.Length - hdrName.Length];
                System.Buffer.BlockCopy(info, hdrName.Length, encryptedBuffer, 0, encryptedBuffer.Length);
            }
            else
            {
                // No key embedded in the text so use the key that was passed in
            }

            ICrypto c = (ICrypto)m_activeCryptoSessions[keyName];
            if (c == null)
            {
                c = addActiveSession(keyName);
            }

            return c.decrypt(encryptedBuffer);
        }

        public string decrypt(string info)
        {
            return decrypt(m_defaultKeyName, info);
        }

        public string decrypt(string name, string info)
        {
            string encryptedString = info;

            // Initialization
            String keyName = name;

            // Determine if a key exists in the header
            Int32 index = info.IndexOf(END_IDENTIFIER);
            if (index > 0)
            {
                // Get the key name from the encrypted data
                keyName = encryptedString.Substring(0, index);
                keyName = keyName.Substring(BEGIN_IDENTIFIER.Length);

                encryptedString = info.Substring(index + END_IDENTIFIER.Length);
            }
            else
            {
                // use the key name passed in if it wasn't embedded in the message
            }

            ICrypto c = (ICrypto)m_activeCryptoSessions[keyName];
            if (c == null)
            {
                c = addActiveSession(keyName);
            }

            return c.decrypt(encryptedString);
        }

        public string encrypt(string info)
        {
            return encrypt(m_defaultKeyName, info);
        }

        public string encrypt(string name, string info)
        {
            String keyName = string.Empty;

            IKeyInfo kinfo = KeyMaster.getKeyInfo(name);
            if (kinfo != null)
            {
                keyName = kinfo.Name;
            }
            else
            {
                throw new CryptoException("Unable to locate key for encryption");
            }

            ICrypto c = (ICrypto)m_activeCryptoSessions[keyName];
            if (c == null)
            {
                c = addActiveSession(keyName);
            }

            string encryptedData = c.encrypt(info);

            encryptedData = BEGIN_IDENTIFIER + keyName + END_IDENTIFIER + encryptedData;

            return encryptedData;
        }

        public byte[] encrypt(byte[] info)
        {
            return encrypt(m_defaultKeyName, info);
        }

        public byte[] encrypt(string name, byte[] info)
        {
            string keyName = string.Empty;

            IKeyInfo kinfo = KeyMaster.getKeyInfo(name);
            if (kinfo != null)
            {
                keyName = kinfo.Name;
            }
            else
            {
                throw new CryptoException("Unable to locate key for encryption");
            }

            ICrypto c = (ICrypto)m_activeCryptoSessions[keyName];
            if (c == null)
            {
                c = addActiveSession(keyName);
            }

            // Encrypt the data
            byte[] encryptedData = c.encrypt(info);

            // Stash the key name in the header of the encrypted data
            byte[] hdrName = Encoding.UTF8.GetBytes(BEGIN_IDENTIFIER + keyName + END_IDENTIFIER);
            byte[] combinedData = new byte[hdrName.Length + encryptedData.Length];
            System.Buffer.BlockCopy(hdrName, 0, combinedData, 0, hdrName.Length);
            System.Buffer.BlockCopy(encryptedData, 0, combinedData, hdrName.Length, encryptedData.Length);

            return combinedData;
        }

        public Stream createCryptoStream(Stream source)
        {
            return createCryptoStream(m_defaultKeyName, source);
        }

        public Stream createCryptoStream(string name, Stream source)
        {
            ICrypto c = (ICrypto)m_activeCryptoSessions[name];
            if (c == null)
                c = addActiveSession(name);
            return c.createCryptoStream(source);
        }

        public bool encryptFile(FileInfo sourceFile, FileInfo destFile)
        {
            return encryptFile(m_defaultKeyName, sourceFile, destFile);
        }

        public bool encryptFile(string name, FileInfo sourceFile, FileInfo destFile)
        {
            bool didit = false;

            string keyName = string.Empty;
            IKeyInfo kinfo = KeyMaster.getKeyInfo(name);
            if (kinfo != null)
            {
                keyName = kinfo.Name;
            }
            else
            {
                throw new CryptoException("Unable to locate key for encryption");
            }

            ICrypto c = (ICrypto)m_activeCryptoSessions[keyName];
            if (c == null)
            {
                c = addActiveSession(keyName);
            }
            if (c != null)
            {
                byte[] workBuff = new byte[100];
                long rdlen = 0;
                int len;

                FileStream inStream = sourceFile.OpenRead();
                FileStream outStream = destFile.OpenWrite();

                byte[] hdrName = Encoding.UTF8.GetBytes(BEGIN_IDENTIFIER + keyName + END_IDENTIFIER);
                outStream.Write(hdrName, 0, hdrName.Length);

                CryptoStream cryptoStream = (CryptoStream)c.createCryptoStream(outStream);

                long numBytes = inStream.Length;

                while (rdlen < numBytes)
                {
                    len = inStream.Read(workBuff, 0, 100);
                    cryptoStream.Write(workBuff, 0, len);
                    rdlen = rdlen + len;
                }

                cryptoStream.FlushFinalBlock();

                cryptoStream.Close();
                outStream.Close();
                inStream.Close();
                didit = true;
            }
            return didit;
        }

        public bool decryptFile(FileInfo sourceFile, FileInfo destFile)
        {
            return decryptFile(m_defaultKeyName, sourceFile, destFile);
        }

        public bool decryptFile(string name, FileInfo sourceFile, FileInfo destFile)
        {
            string keyName = name;

            bool didit = false;

            FileStream inStream = sourceFile.OpenRead();

            // Extract out the BEGIN_IDENTIFIER if it exists
            byte[] hdrPrefix = Encoding.UTF8.GetBytes(BEGIN_IDENTIFIER);
            int currPos = inStream.Read(hdrPrefix, 0, hdrPrefix.Length);

            // Convert the initial look up to string to see if it has a header
            string beginId = Encoding.UTF8.GetString(hdrPrefix);
            if (string.Compare(beginId, BEGIN_IDENTIFIER) == 0)
            {
                // This has a header so we need to find the key
                string header = beginId;
                while (true)
                {
                    // Read 100 byte blocks
                    byte[] block = new byte[100];
                    currPos += inStream.Read(block, 0, 100);

                    // Convert block to a string representation
                    string blockString = Encoding.UTF8.GetString(block);

                    // Add block to header data
                    header += blockString;

                    // See if the END_IDENTIFIER has been found
                    int endIdIndex = header.IndexOf(END_IDENTIFIER);
                    if (endIdIndex != -1)
                    {
                        // Found the end of the header

                        // Extract the Key name
                        keyName = header.Substring(beginId.Length, endIdIndex - beginId.Length);

                        // Reset the input stream to the correct location
                        currPos = endIdIndex + END_IDENTIFIER.Length;

                        // Set the stream index to just after the END_IDENTIFIER
                        inStream.Seek(currPos, SeekOrigin.Begin);

                        // break out of the loop, we got what we needed.
                        break;
                    }
                }
            }
            else
            {
                // No header containing keyname in the beginning of stream, seek back to the beginning
                inStream.Seek(0, SeekOrigin.Begin);
            }

            ICrypto c = (ICrypto)m_activeCryptoSessions[keyName];
            if (c == null)
            {
                c = addActiveSession(keyName);
            }

            if (c != null)
            {

                FileStream outStream = destFile.OpenWrite();
                CryptoStream cryptoStream = (CryptoStream)c.createCryptoStream(inStream);

                int len;
                byte[] buffer = new byte[100];
                do
                {
                    len = cryptoStream.Read(buffer, 0, 100);
                    outStream.Write(buffer, 0, len);
                } while (len == 100);

                cryptoStream.Close();
                outStream.Close();
                inStream.Close();
                didit = true;
            }

            return didit;
        }

        public bool EncryptMacroGeneratesDecryptMacro
        {
            get { return CryptoManager.EncryptMacroGenDecrypt; }
            set { CryptoManager.EncryptMacroGenDecrypt = value; }
        }

        public bool isValueEncrypted(string val)
        {
            return (val == null) ? false : val.StartsWith(BEGIN_IDENTIFIER);
        }
        #endregion

        #region ICryptoManagerEx Members

        /// <summary>
        /// Encrypt file contents using a key stored in the system container
        /// </summary>
        public Boolean encryptFileEx(String keyContainerName, FileInfo sourceFile, FileInfo destFile)
        {
            // Read the bytes of data
            byte[] dataToEncrypt = new byte[sourceFile.Length];
            FileStream fs = sourceFile.OpenRead();
            for (Int32 i = 0; i < fs.Length; i++)
                dataToEncrypt[i] = (byte)fs.ReadByte();
            fs.Close();

            return encryptFileEx(keyContainerName, dataToEncrypt, destFile.ToString());
        }

        /// <summary>
        /// Encrypt file contents using a key stored in the system container
        /// </summary>
        public Boolean encryptFileEx(String keyContainerName, String input, String destPath)
        {
            // Create a Encoder to convert between byte array and string.
            byte[] dataToEncrypt = Encoding.ASCII.GetBytes(input);
            return encryptFileEx(keyContainerName, dataToEncrypt, destPath);
        }

        /// <summary>
        /// Encrypt file contents using a key stored in the system container
        /// </summary>
        public Boolean encryptFileEx(String keyContainerName, byte[] input, String destPath)
        {
            // Create the CspParameters object and set the key container 
            // name used to store the RSA key pair.
            CspParameters cp = new CspParameters();
            cp.KeyContainerName = keyContainerName;

            //Create a new instance of RSACryptoServiceProvider using the CspParameters
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(cp);

            // by default this will create a 128dataToEncrypt bits AES (Rijndael) object 
            SymmetricAlgorithm sa = SymmetricAlgorithm.Create();
            ICryptoTransform ct = sa.CreateEncryptor();
            byte[] encrypt = ct.TransformFinalBlock(input, 0, input.Length);

            RSAPKCS1KeyExchangeFormatter fmt = new RSAPKCS1KeyExchangeFormatter(rsa);
            byte[] keyex = fmt.CreateKeyExchange(sa.Key);

            // return the key exchange, the IV (public) and encrypted data 
            byte[] result = new byte[keyex.Length + sa.IV.Length + encrypt.Length];
            Buffer.BlockCopy(keyex, 0, result, 0, keyex.Length);
            Buffer.BlockCopy(sa.IV, 0, result, keyex.Length, sa.IV.Length);
            Buffer.BlockCopy(encrypt, 0, result, keyex.Length + sa.IV.Length, encrypt.Length);

            // Make sure the file is not read-only before attempting to update
            if (File.Exists(destPath))
                File.SetAttributes(destPath, FileAttributes.Normal);

            // Create the encrypted file and write the contents
            FileStream fs = new FileStream(destPath, FileMode.Create);
            fs.Write(result, 0, result.Length);

            // Close the stream
            fs.Close();
            return true;
        }

        public bool encryptFileEx(FileInfo sourceFile, FileInfo destFile)
        {
            // Read the bytes of data
            byte[] dataToEncrypt = new byte[sourceFile.Length];
            FileStream fs = sourceFile.OpenRead();
            for (Int32 i = 0; i < fs.Length; i++)
                dataToEncrypt[i] = (byte)fs.ReadByte();
            fs.Close();

            return encryptFileEx(dataToEncrypt, destFile.ToString());
        }

        public bool encryptFileEx(string input, string destPath)
        {
            // Create a Encoder to convert between byte array and string.
            byte[] dataToEncrypt = Encoding.ASCII.GetBytes(input);
            return encryptFileEx(dataToEncrypt, destPath);
        }

        public bool encryptFileEx(byte[] input, string destPath)
        {
#if NET_1_1
			return encryptFileEx(FileContainerName, input, destPath);
#else
            byte[] encryptedInput = ProtectedData.Protect(input, s_additionalEntropy, DataProtectionScope);

            // Create the encrypted file and write the contents
            FileStream fs = new FileStream(destPath, FileMode.Create);
            fs.Write(encryptedInput, 0, encryptedInput.Length);

            // Close the stream
            fs.Close();
            return true;
#endif
        }

        /// <summary>
        /// Decrypt file contents using a key stored in the system container
        /// </summary>
        public String decryptFileEx(String keyContainerName, String sourcePath)
        {
            // Load the file
            FileStream fs = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);

            // Read the bytes of data
            byte[] encryptedData = new byte[fs.Length];
            for (Int32 i = 0; i < fs.Length; i++)
                encryptedData[i] = (byte)fs.ReadByte();

            // Close the stream
            fs.Close();

            // Create the CspParameters object and set the key container 
            // name used to store the RSA key pair.
            CspParameters cp = new CspParameters();
            cp.KeyContainerName = keyContainerName;

            //Create a new instance of RSACryptoServiceProvider using the CspParameters
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(cp);

            // by default this will create a 128 bits AES (Rijndael) object 
            SymmetricAlgorithm sa = SymmetricAlgorithm.Create();

            byte[] keyex = new byte[rsa.KeySize >> 3];
            Buffer.BlockCopy(encryptedData, 0, keyex, 0, keyex.Length);

            RSAPKCS1KeyExchangeDeformatter def = new RSAPKCS1KeyExchangeDeformatter(rsa);
            byte[] key = def.DecryptKeyExchange(keyex);

            byte[] iv = new byte[sa.IV.Length];
            Buffer.BlockCopy(encryptedData, keyex.Length, iv, 0, iv.Length);

            ICryptoTransform ct = sa.CreateDecryptor(key, iv);
            byte[] decrypt = ct.TransformFinalBlock(encryptedData, keyex.Length + iv.Length, encryptedData.Length - (keyex.Length + iv.Length));

            // Create a Encoder to convert between byte array and string. 
            String decryptedContent = Encoding.ASCII.GetString(decrypt);

            return decryptedContent;
        }

        public string decryptFileEx(string sourcePath)
        {
#if NET_1_1
			return decryptFileEx(FileContainerName, sourcePath);
#else
            FileStream fs = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);

            // Read the bytes of data
            byte[] encryptedData = new byte[fs.Length];
            for (Int32 i = 0; i < fs.Length; i++)
                encryptedData[i] = (byte)fs.ReadByte();

            // Close the stream
            fs.Close();

            byte[] decryptedData = null;
            try
            {
                decryptedData = ProtectedData.Unprotect(encryptedData, s_additionalEntropy, DataProtectionScope);
            }
            catch (CryptographicException e)
            {
                Application.currentApp.Log.println("Crypto exception: {0}", e.ToString());
                throw;
            }
            catch (PlatformNotSupportedException e)
            {
                Application.currentApp.Log.println("Crypto exception: {0}", e.ToString());
                throw;
            }
            catch (Exception e)
            {
                Application.currentApp.Log.println("Crypto exception: {0}", e.ToString());
                throw;
            }

            // Create a Encoder to convert between byte array and string. 
            String decryptedContent = Encoding.ASCII.GetString(decryptedData);

            return decryptedContent;
#endif
        }

        #endregion

        #region IConfigPartner Members

        public void checkConfigStatus()
        {
            if (m_keyClass == null)
                throw new BadConfigException("No <Cryptography><KeyMaster> was properly defined for cryptography");
			m_keys = (BaseKeyMaster)Cmn.callCtor(m_keyClass);

			if (m_keys == null)
				throw new BadConfigException("No <Cryptography><KeyMaster> was properly defined for cryptography");
		}

        public void registerForConfig(IConfigManager config)
        {
            config.registerHandler("...<Cryptography>", new CryptoOptionsHandler(this));
            config.registerHandler("...<Cryptography> <KeyMaster>", new CryptoKeyMasterHandler(this));
#if !NET_1_1
            config.registerHandler("...<X509CryptoClass>", new CryptoX509CryptoClassHandler(this));
            config.registerHandler("...<Certificates> <Certificate>", new CryptoCertificatesHandler(this));
#endif
        }

        #endregion

        public class CryptoOptionsHandler : ConfigHandler
        {
            protected static string[] s_legalAttributes = new string[] { "defaultKey" };
            private CryptoManager m_mgr;
            public CryptoOptionsHandler(CryptoManager mgr) { m_mgr = mgr; }
            public override void executeBefore(IConfigElement configInfo)
            {
                //IApplication app = (IApplication)configInfo.Context["app"];
                configInfo.checkIllegalAttributes(s_legalAttributes);
                string defaultKey = configInfo.getAttribute("defaultKey", "");
                m_mgr.DefaultKey = defaultKey;
                string useRandomKeys = Application.currentApp.resMgr.resolve(USE_RANDOM_KEYS);
                if (useRandomKeys != USE_RANDOM_KEYS)
                    m_mgr.UseRandomKeys = Convert.ToBoolean(useRandomKeys);
            }
        }

        public class CryptoKeyMasterHandler : ConfigHandler
        {
            protected static string[] s_legalAttributes = new string[] { };
            private CryptoManager m_mgr;
            public CryptoKeyMasterHandler(CryptoManager mgr) { m_mgr = mgr; }
            public override void executeBefore(IConfigElement configInfo)
            {
                //IApplication app = (IApplication)configInfo.Context["app"];
                configInfo.checkIllegalAttributes(s_legalAttributes);
                string keyMaster = configInfo.getChildText();
                if (keyMaster == null || keyMaster.Length == 0)
                    this.xmlError(configInfo.Context, "keyMaster attribute must be specified and be an appropriate class name that implement IKeyMaster");
                m_mgr.setKeyMaster(keyMaster);
            }
        }

        #if !NET_1_1

        public class CryptoX509CryptoClassHandler : ConfigHandler
        {
            protected static string[] s_legalAttributes = new string[] { };
            private CryptoManager m_mgr;
            
            public CryptoX509CryptoClassHandler(CryptoManager mgr) 
            { 
                m_mgr = mgr; 
            }
            
            public override void executeBefore(IConfigElement configInfo)
            {
                configInfo.checkIllegalAttributes(s_legalAttributes);

                string x509Crypto = configInfo.getChildText();
                if (x509Crypto == null || x509Crypto.Length == 0)
                {
                    this.xmlError(configInfo.Context, "X509CryptoClass element must be specified and be an appropriate class name that implement IX509Crypto");
                }

                // Set the instance of the Crypto provider
                m_mgr.X509XmlCrypto = Cmn.callCtor(x509Crypto) as IX509XmlCrypto;
            }
        }

        public class CryptoCertificatesHandler : ConfigHandler
        {
            protected static string[] s_legalAttributes = new string[] { "name", "certFilePath", "pfxFilePath", "pfxFilePwd" };
            private CryptoManager m_mgr;

            public CryptoCertificatesHandler(CryptoManager mgr) 
            { 
                m_mgr = mgr; 
            }
            
            public override void executeBefore(IConfigElement configInfo)
            {
                configInfo.checkIllegalAttributes(s_legalAttributes);

                String name = configInfo.getAttribute("name", String.Empty);
                if (name != String.Empty)
                {
                    String certFilePath = configInfo.getAttribute("certFilePath", String.Empty);
                    String pfxFilePath = configInfo.getAttribute("pfxFilePath", String.Empty);
                    String pfxFilePwd = configInfo.getAttribute("pfxFilePwd", String.Empty);

                    ICertificateInfo certInfo = new CertificateInfo(name, certFilePath, pfxFilePath, pfxFilePwd);
                    if (m_mgr.X509XmlCrypto != null)
                    {
                        m_mgr.X509XmlCrypto.Certificates[certInfo.Name] = certInfo;
                    }
                }
                else
                {
                    Application.globalApp.Log.println("Specified Certificate does not have a name and can not be used.");
                }
            }
        }
        #endif
        
    }
}
